﻿using ModelLayer.DTO;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;

namespace ModelLayer.Graph
{
    [DataContract]
    public class StationNetworkGraph
    {
        [DataMember]
        private IList<StationNode> allNodes;
        [DataMember]
        private BatteryType type;
        [DataMember]
        private StationNode startNode;
        [DataMember]
        private StationNode endNode;

        public StationNetworkGraph(BatteryType batteryType)
        {
            type = batteryType;
            allNodes = new List<StationNode>();
        }

        /// <summary>
        /// Adds multiple nodes to the graph and automatically creates edges between them, withing distance limit.
        /// </summary>
        /// <param name="stations">Stations to be added to the graph. The amount last two nodes are start and end nodes. 
        /// Therefore the amount of stations must be >=2. Those nodes can be null.</param>
        /// <param name="distanceMatrixes">Distances between stations sorted in the same order as stations. 
        /// The method is not checking if the order of parameters is correct, therefore it is developer responsibility 
        /// to ensure that. If amount of matrixes is different than amount of stations and exception will be thrown. 
        /// Also exception will be thrown if the length of matrixes is different than amount of stations.</param>
        /// <param name="timeMatrixes">Times to travel between stations sorted in the same order as stations. 
        /// Has the same error handling as distanceMatrixes</param>
        public void SetMultipleNodes(IList<Station> stations, double[][] distanceMatrixes, double[][] timeMatrixes)
        {
            if (stations.Count == distanceMatrixes.Length && stations.Count == timeMatrixes.Length)
            {
                int length = stations.Count;
                for (int i = 0; i < length; i++)
                {
                    allNodes.Add(new StationNode() { Marked = false, Station = StationDTO.CreateDTO(stations[i], type), Id = allNodes.Count });      //add a node
                }
                
                for (int i = 0; i < length; i++)
                {
                    SetMultipleEdges(allNodes[i], distanceMatrixes[i], timeMatrixes[i]);    //set all edges for Nodes
                }
                //set start and end node
                startNode = allNodes[length - 2];   
                endNode = allNodes[length - 1];
                
            }
            else
            {
                throw new Exception("Stations and amount of distance matrixes do not match in count");
            }
        }
        /// <summary>
        /// Adds node to the graph and automatically creates all edges within distance limit.
        /// </summary>
        /// <param name="station">Name of station to be added, when I implment model layer I will change it to actuall Station</param>
        /// <param name="distanceMatrix">Distances between stations in correct order. The method is not able to check in any way if 
        /// distances are provided in correct order, so it is developer responsibility to ensure it. 
        /// If provided matrix is of different size than size of graph, exception will be thrown.</param>
        /// <param name="timeMatrix">Times to travel between stations sorted in the same order as stations. 
        /// Has the same error handling as distanceMatrix</param>
        public void AddNode(Station station, double[] distanceMatrix, double[] timeMatrix)
        {
            if (allNodes.Count + 1 != distanceMatrix.Length)
            {
                throw new Exception("Stations and amount of distance matrixes do not match in count");
            }
            int edgesAdded = 0; //just for debugging
            StationNode node = new StationNode() { Station = StationDTO.CreateDTO(station, type), Marked = false, Id = allNodes.Count };
            allNodes.Add(node);
            int length = allNodes.Count;
            for (int i = 0; i < length; i++)
            {
                edgesAdded += TryToAddTwoSidedEdge(node, allNodes[i], distanceMatrix[i], timeMatrix[i]) ? 1 : 0;
            }
            //Debug.Print("Edges added to the graph: " + edgesAdded);

        }


        /// <summary>
        /// Sets all edges of specified node. 
        /// </summary>
        /// <param name="node">Node to which the edges will be added</param>
        /// <param name="distanceMatrix">Distances between stations in correct order. The method is not able to check in any way if 
        /// distances are provided in correct order, so it is developer responsibility to ensure it. 
        /// If provided matrix is of different size than size of graph, exception will be thrown.</param>
        private void SetMultipleEdges(StationNode node, double[] distanceMatrix, double[] timeMatrix)
        {
            if (allNodes.Count != distanceMatrix.Length)
            {
                throw new Exception("Stations and distance matrix length do not match in count, station: "
                    + node.Station + " count " + distanceMatrix.Length);
            }
            int edgesAdded = 0; //just for debugging
            int length = allNodes.Count;
            for (int i = 0; i < length; i++)
            {
                TryToAddEdge(node, allNodes[i], distanceMatrix[i], timeMatrix[i]);
            }
            //Debug.Print("Edges added to the graph: " + edgesAdded);
        }

        /// <summary>
        /// Adds the edge to a node if the distance is within limit.
        /// </summary>
        /// <param name="startNode">The node from which the edge begins.</param>
        /// <param name="endNode">The node where the edge leads to.</param>
        /// <param name="distance">"Distance between the stations. If higher than maximum distance, the edge will not be added.</param>
        /// <returns>True if edge was added, false otherwise</returns>
        private bool TryToAddEdge(StationNode startNode, StationNode endNode, double distance, double time)
        {
            if (distance <= type.maximum_distance && distance != 0)
            {
                AddEdge(startNode, endNode, distance, time);
                return true;
            }
            return false;
        }

        /// <summary>
        /// Adds the edge to a node if the distance is within limit.
        /// Also adds a return edge to the end node.
        /// </summary>
        /// <param name="startNode">The node from which the edge begins.</param>
        /// <param name="endNode">The node where the edge leads to.</param>
        /// <param name="distance">"Distance between the stations. If higher than maximum distance, the edge will not be added.</param>
        /// <returns>True if edge was added, false otherwise</returns>
        private bool TryToAddTwoSidedEdge(StationNode startNode, StationNode endNode, double distance, double time)
        {
            if (distance <= type.maximum_distance && distance != 0)
            {
                AddEdge(startNode, endNode, distance, time);
                AddEdge(endNode, startNode, distance, time);
                return true;
            }
            return false;
        }

        private void AddEdge(StationNode startNode, StationNode endNode, double distance, double time)
        {
            Edge edge = new Edge() { StartNode = startNode, EndNode = endNode, Weights = new Dictionary<Edge.WeightType, double>() };
            edge.AddWeight(Edge.WeightType.DISTANCE, distance);
            edge.AddWeight(Edge.WeightType.TIME, time);
            double priceForFullBattery = 0;
            if(startNode.Station != null)
            {
                priceForFullBattery = startNode.Station.full_battery_cost;
            }
            edge.AddWeight(Edge.WeightType.COST, priceForFullBattery * distance / type.maximum_distance);
            startNode.AddEdge(edge);
        }

        private bool RemoveNode(StationNode node)
        {

            bool removed = allNodes.Remove(node);
            foreach (StationNode n in allNodes)
            {
                n.RemoveEdgeTo(node);
            }
            return removed;
        }

        public LinkedList<StationNode> GetShortestRoute(Edge.WeightType weightType)
        {
            Dictionary<StationNode, double> weights = new Dictionary<StationNode, double>();
            Dictionary<StationNode, LinkedList<StationNode>> nodesOnTheWay = new Dictionary<StationNode,LinkedList<StationNode>>();
            foreach (StationNode node in allNodes)
            {
                nodesOnTheWay.Add(node, new LinkedList<StationNode>());
                weights.Add(node, double.PositiveInfinity);
            }
            IList<StationNode> nodesLeft = new List<StationNode>(allNodes);
            //nodesLeft.Remove(startNode);
            StationNode currentNode;
            weights[startNode] = 0;
            
            while (nodesLeft.Count > 0)
            {
                currentNode = weights.Where(a => nodesLeft.Contains(a.Key) && 
                    weights.Where(w => nodesLeft.Contains(w.Key)).Select(p => p.Value).Min() == a.Value).FirstOrDefault().Key;
                if (currentNode == null)
                {
                    //Debug.WriteLine("Current node is null");
                    break;
                }
                nodesLeft.Remove(currentNode);

                //Debug.WriteLine("Running loop, currentNode => " + (currentNode.Station != null ? currentNode.Station.name : "rand point"));
                LinkedList<Edge> currentEdges = currentNode.Edges;
                foreach(Edge edge in currentEdges)
                {
                    double weight = edge.Weights[weightType] + weights[currentNode];
                    StationNode outcomingNode = edge.EndNode;
                    if(weight < weights[outcomingNode])
                    {
                        weights[outcomingNode] = weight;
                        LinkedList<StationNode> nodesSoFar = new LinkedList<StationNode>(nodesOnTheWay[currentNode]);
                        nodesSoFar.AddLast(currentNode);
                        nodesOnTheWay[outcomingNode] = nodesSoFar;
                    }
                }
                
                
                //if (nodesLeft.Count == 0 || currentNode == null)
                //{
                //    break;
                //}
                
            }
            //Debug.WriteLine("Nodes found so far => " + nodesOnTheWay[endNode].Count + " nodes left => " + nodesLeft.Count);
            return nodesOnTheWay[endNode];
        }

        public override string ToString()  //For debugging purposes
        {
            string toString = "";
            foreach (StationNode node in allNodes)
            {
                string startName = "Start node ";
                if(node.Station != null)
                {
                    startName = node.Station.name;
                }
                toString += startName+ ": (";
                foreach (Edge edge in node.Edges)
                {
                    string endName = "End point";
                    if(edge.EndNode.Station != null)
                    {
                        endName = edge.EndNode.Station.name;
                    }
                    toString += " " + edge.Weights[Edge.WeightType.DISTANCE].ToString() + " => " + endName + ", ";
                }
                toString += ");\n\n";
            }

            return toString;
        }
    }
}
